﻿#if (!PCL) && ((!UNITY_5) || UNITY_STANDALONE)

using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Debugging;
using MoonSharp.VsCodeDebugger.DebuggerLogic;
using MoonSharp.VsCodeDebugger.SDK;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace MoonSharp.VsCodeDebugger
{
    /// <summary>
    /// Class implementing a debugger allowing attaching from a Visual Studio Code debugging session.
    /// </summary>
    public class MoonSharpVsCodeDebugServer : IDisposable
    {
        object m_Lock = new object();
        List<AsyncDebugger> m_DebuggerList = new List<AsyncDebugger>();
        public AsyncDebugger m_Current = null;
        ManualResetEvent m_StopEvent = new ManualResetEvent(false);
        bool m_Started = false;
        int m_Port;

        /// <summary>
        /// Initializes a new instance of the <see cref="MoonSharpVsCodeDebugServer" /> class.
        /// </summary>
        /// <param name="port">The port on which the debugger listens. It's recommended to use 41912.</param>
        public MoonSharpVsCodeDebugServer(int port = 41912)
        {
            m_Port = port;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MoonSharpVsCodeDebugServer" /> class with a default script.
        /// Note that for this specific script, it will NOT attach the debugger to the script.
        /// </summary>
        /// <param name="script">The script object to debug.</param>
        /// <param name="port">The port on which the debugger listens. It's recommended to use 41912 unless you are going to keep more than one script object around.</param>
        /// <param name="sourceFinder">A function which gets in input a source code and returns the path to
        /// source file to use. It can return null and in that case (or if the file cannot be found)
        /// a temporary file will be generated on the fly.</param>
        [Obsolete("Use the constructor taking only a port, and the 'Attach' method instead.")]
        public MoonSharpVsCodeDebugServer(Script script, int port, Func<SourceCode, string> sourceFinder = null)
        {
            m_Port = port;
            m_Current = new AsyncDebugger(script, sourceFinder ?? (s => s.Name), "Default script");
            m_DebuggerList.Add(m_Current);
        }

        /// <summary>
        /// Attaches the specified script to the debugger
        /// </summary>
        /// <param name="script">The script.</param>
        /// <param name="name">The name of the script.</param>
        /// <param name="sourceFinder">A function which gets in input a source code and returns the path to
        /// source file to use. It can return null and in that case (or if the file cannot be found)
        /// a temporary file will be generated on the fly.</param>
        /// <exception cref="ArgumentException">If the script has already been attached to this debugger.</exception>
        public void AttachToScript(Script script, string name, Func<SourceCode, string> sourceFinder = null)
        {
            lock (m_Lock)
            {
                if (m_DebuggerList.Any(d => d.Script == script))
                    throw new ArgumentException("Script already attached to this debugger.");

                var debugger = new AsyncDebugger(script, sourceFinder ?? (s => s.Name), name);
                script.AttachDebugger(debugger);
                m_DebuggerList.Add(debugger);

                if (m_Current == null)
                    m_Current = debugger;
            }
        }

        /// <summary>
        /// Gets a list of the attached debuggers by id and name
        /// </summary>
        public IEnumerable<KeyValuePair<int, string>> GetAttachedDebuggersByIdAndName()
        {
            lock (m_Lock)
                return m_DebuggerList
                    .OrderBy(d => d.Id)
                    .Select(d => new KeyValuePair<int, string>(d.Id, d.Name))
                    .ToArray();
        }


        /// <summary>
        /// Gets or sets the current script by ID (see GetAttachedDebuggersByIdAndName). 
        /// New vscode connections will attach to this debugger ID. Changing the current ID does NOT disconnect
        /// connected clients.
        /// </summary>
        public int? CurrentId
        {
            get
            {
                lock (m_Lock) return m_Current != null ? m_Current.Id : (int?) null;
            }
            set
            {
                lock (m_Lock)
                {
                    if (value == null)
                    {
                        m_Current = null;
                        return;
                    }

                    var current = (m_DebuggerList.FirstOrDefault(d => d.Id == value));

                    if (current == null)
                        throw new ArgumentException("Cannot find debugger with given Id.");

                    m_Current = current;
                }
            }
        }


        /// <summary>
        /// Gets or sets the current script. New vscode connections will attach to this script. Changing the current script does NOT disconnect
        /// connected clients.
        /// </summary>
        public Script Current
        {
            get
            {
                lock (m_Lock) return m_Current != null ? m_Current.Script : null;
            }
            set
            {
                lock (m_Lock)
                {
                    if (value == null)
                    {
                        m_Current = null;
                        return;
                    }

                    var current = (m_DebuggerList.FirstOrDefault(d => d.Script == value));

                    if (current == null)
                        throw new ArgumentException("Cannot find debugger with given script associated.");

                    m_Current = current;
                }
            }
        }

        /// <summary>
        /// Detaches the specified script. The debugger attached to that script will get disconnected.
        /// </summary>
        /// <param name="script">The script.</param>
        /// <exception cref="ArgumentException">Thrown if the script cannot be found.</exception>
        public void Detach(Script script)
        {
            lock (m_Lock)
            {
                var removed = m_DebuggerList.FirstOrDefault(d => d.Script == script);

                if (removed == null)
                    throw new ArgumentException("Cannot detach script - not found.");

                removed.Client = null;

                m_DebuggerList.Remove(removed);

                if (m_Current == removed)
                {
                    if (m_DebuggerList.Count > 0)
                        m_Current = m_DebuggerList[m_DebuggerList.Count - 1];
                    else
                        m_Current = null;
                }
            }
        }

        /// <summary>
        /// Gets or sets a delegate which will be called when logging messages are generated
        /// </summary>
        public Action<string> Logger { get; set; }


        /// <summary>
        /// Gets the debugger object. Obsolete, use the new interface using the Attach method instead.
        /// </summary>
        // [Obsolete("Use the Attach method instead.")]
        public IDebugger GetDebugger()
        {
            lock (m_Lock)
                return m_Current;
        }

        /// <summary>
        /// Stops listening
        /// </summary>
        /// <exception cref="InvalidOperationException">Cannot stop; server was not started.</exception>
        public void Dispose()
        {
            m_StopEvent.Set();
        }

        /// <summary>
        /// Starts listening on the localhost for incoming connections.
        /// </summary>
        public MoonSharpVsCodeDebugServer Start()
        {
            lock (m_Lock)
            {
                if (m_Started)
                    throw new InvalidOperationException("Cannot start; server has already been started.");

                m_StopEvent.Reset();

                TcpListener serverSocket = null;
                serverSocket = new TcpListener(IPAddress.Parse("127.0.0.1"), m_Port);
                serverSocket.Start();

                SpawnThread("VsCodeDebugServer_" + m_Port.ToString(), () => ListenThread(serverSocket));

                m_Started = true;

                return this;
            }
        }


        private void ListenThread(TcpListener serverSocket)
        {
            try
            {
                while (!m_StopEvent.WaitOne(0))
                {
#if DOTNET_CORE
					var task = serverSocket.AcceptSocketAsync();
					task.Wait();
					var clientSocket = task.Result;
#else
                    var clientSocket = serverSocket.AcceptSocket();
#endif

                    if (clientSocket != null)
                    {
                        string sessionId = Guid.NewGuid().ToString("N");
                        Log("[{0}] : Accepted connection from client {1}", sessionId, clientSocket.RemoteEndPoint);

                        SpawnThread("VsCodeDebugSession_" + sessionId, () =>
                        {
                            using (var networkStream = new NetworkStream(clientSocket))
                            {
                                try
                                {
                                    RunSession(sessionId, networkStream);
                                }
                                catch (Exception ex)
                                {
                                    Log("[{0}] : Error : {1}", ex.Message);
                                }
                            }

#if DOTNET_CORE
							clientSocket.Dispose();
#else
                            clientSocket.Close();
#endif
                            Log("[{0}] : Client connection closed", sessionId);
                        });
                    }
                }
            }
            catch (Exception e)
            {
                Log("Fatal error in listening thread : {0}", e.Message);
            }
            finally
            {
                if (serverSocket != null)
                    serverSocket.Stop();
            }
        }

        public bool Connected
        {
            get
            {
                if (m_Current == null)
                    return false;

                if (m_Current.Client == null)
                    return false;

                return true;
            }
        }


        private void RunSession(string sessionId, NetworkStream stream)
        {
            DebugSession debugSession = null;

            lock (m_Lock)
            {
                if (m_Current != null)
                {
                    debugSession = new MoonSharpDebugSession(this, m_Current);
                    // Connected = true;
                }
                else
                {
                    debugSession = new EmptyDebugSession(this);
                }
            }


            debugSession.ProcessLoop(stream, stream);
        }

        private void Log(string format, params object[] args)
        {
            Action<string> logger = Logger;

            if (logger != null)
            {
                string msg = string.Format(format, args);
                logger(msg);
            }
        }


        private static void SpawnThread(string name, Action threadProc)
        {
#if DOTNET_CORE
			System.Threading.Tasks.Task.Run(() => threadProc());
#else
            new System.Threading.Thread(() => threadProc())
                {
                    IsBackground = true,
                    Name = name
                }
                .Start();
#endif
        }
    }
}

#else
						using System;
using System.Collections.Generic;
using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Debugging;

namespace MoonSharp.VsCodeDebugger
{
	public class MoonSharpVsCodeDebugServer : IDisposable
	{
		public MoonSharpVsCodeDebugServer(int port = 41912)
		{
		}

		[Obsolete("Use the constructor taking only a port, and the 'Attach' method instead.")]
		public MoonSharpVsCodeDebugServer(Script script, int port, Func<SourceCode, string> sourceFinder = null)
		{
		}

		public void AttachToScript(Script script, string name, Func<SourceCode, string> sourceFinder = null)
		{
		}

		public IEnumerable<KeyValuePair<int, string>> GetAttachedDebuggersByIdAndName()
		{
			yield break;
		}


		public int? CurrentId
		{
			get { return null; }
			set { }
		}


		public Script Current
		{
			get { return null; }
			set { }
		}

		/// <summary>
		/// Detaches the specified script. The debugger attached to that script will get disconnected.
		/// </summary>
		/// <param name="script">The script.</param>
		/// <exception cref="ArgumentException">Thrown if the script cannot be found.</exception>
		public void Detach(Script script)
		{

		}

		public Action<string> Logger { get; set; }

		
		[Obsolete("Use the Attach method instead.")]
		public IDebugger GetDebugger()
		{
			return null;
		}

		public void Dispose()
		{
		}

		public MoonSharpVsCodeDebugServer Start()
		{
			return this;
		}

	}
}
#endif